[Java] 使用EnumSet代替位运算简化代码逻辑


位运算

在Review代码时候,看到一段涉及到USB的逻辑代码,他是这样写的

private boolean isUsbConnected;
private boolean isUsbModeNCM;
private boolean isUsbModeAccessory;
private boolean isUsbModeAdb;
private boolean isUsbModeMTP;
...

然后代码逻辑里是大量的成员变量的判断,显得非常臃肿而且难读懂,大量的if-else判断让代码逻辑很脆弱,稍微一个情况没考虑好就会出现难以排查的bug。

所以这种情况使用位掩码进行处理会更简单:

// 博客地址:wossoneri.github.io
private static final int FLAG_USB_CONNECTED = 0x1;
private static final int FLAG_USB_MODE_NCM = 0x1 << 1;
private static final int FLAG_USB_MODE_ACY = 0x1 << 2;
private static final int FLAG_USB_MODE_ADB = 0x1 << 3;
private static final int FLAG_USB_MODE_MTP = 0x1 << 4;
...
private int mUsbState;

public void addUsbState(int flag) {
     mUsbState |= flag; 
}

public void removeUsbState(int flag) {
  mUsbState &= ~flag;
}

public boolean isUsbStateEnable(int flag) {
  return (mUsbState & flag) == flag;
}

简单分析一下这样写的好处:

FLAG_USB_CONNECTED = 0001

FLAG_USB_MODE_NCM = 0010

FLAG_USB_MODE_ACY = 0100

FLAG_USB_MODE_ADB = 1000

通过移位,使得每一位都有独立的代表的意义,1代表enable,0代表disable。

如果要添加状态(Java里int值默认赋值为0):

public void addUsbState(int flag) {
     mUsbState |= flag; 
}

假设添加accessory状态FLAG_USB_MODE_ACY

0000 |= 0100 -> 0100

所以mUsbState就是0100的状态了。

继续添加FLAG_USB_MODE_ADB状态

0100 |= 1000 -> 1100

也可以一次添加多个状态,比如上面的两个状态在一次设置同时添加:

addUsbState(FLAG_USB_MODE_ACY | FLAG_USB_MODE_ADB);

结果就是:

0000 |= (0100 | 1000)
-> 0000 |= 1100
-> 1100

如果是原来的boolean变量,就需要单独为每一个变量设置,就会很麻烦。


然后是移除状态

public void removeUsbState(int flag) {
  mUsbState &= ~flag;
}

比如接着上面移除FLAG_USB_MODE_ADB状态

1100 &= ~1000
-> 1100 &= 0111
-> 0100

如果移除一个不存在的状态比如FLAG_USB_MODE_NCM

0100 &= ~0010
-> 0100 &= 1101
-> 0100

可以看到并不会对当前状态造成任何影响。


最后看一下检查状态

public boolean isUsbStateEnable(int flag) {
  return (mUsbState & flag) == flag;
}

首先检查一下当前拥有的状态:

(0100 & 0100) == 0100
-> 0100 == 0100
-> true

可以检测到该状态。然后换一个状态:

(0100 & 1000) == 1000
-> 0000 == 1000
-> false

没有检测到该状态。

所以,通过三个简单的方法,就可以检查一个变量里保存的所有状态,避免了使用大量bool变量进行挨个检查。简化了代码,增加代码可读性,并且使代码更加稳定。

进阶!使用EnumSet替代位运算

到这里你可能觉得问题解决了就完了,但是还没有!

实际上,《Effective Java》这本书有对位域的一项讨论:

位域表示法也允许利用位操作,有效的执行像union和intersection这样的集合操作。但位域有着int枚举常量所有的缺点,甚至更多。当位域以数字形式打印时,翻译位域比翻译简单的int枚举常量要困难很多。甚至要遍历位域表示的所有元素也没有很容易的方法。

Java.util包提供了EnumSet类来有效地表示从单个枚举类型中提取的多个值的多个集合。这个类实现Set接口,提供丰富的功能、类型安全性以及可从其他Set实现中得到的互用性。

内部实现上,每个EnumSet内容都表示为位矢量,一般(低于64个元素)整个EnumSet就是用一个long的位运算来表示的。也就是说它替你使用位算法实现了这一切,避免你自己写位运算导致代码难读懂的情况。

下面是用EnumSet修改后的示例代码,它更加简短,清楚也更安全。

// 博客地址:wossoneri.github.io
public class UsbManager {

    private EnumSet mUsbState = EnumSet.noneOf(UsbFlags.class);

    public enum UsbFlags {
        CONNECTED, NCM, ACCESSORY, ADB, MTP
    }

    public void addFlag(UsbFlags flag) {
        mUsbState.add(flag);
        System.out.println("After add flag " + flag + ", Now state is " + this.mUsbState);
    }

    public void addFlag(Set flags) {
        mUsbState.addAll(flags);
        System.out.println("After add flags " + flags + ", Now state is " + this.mUsbState);
    }

    public void removeFlag(UsbFlags flag) {
        mUsbState.remove(flag);
        System.out.println("After remove flag " + flag + ", Now state is " + this.mUsbState);
    }

    public void removeFlag(Set flags) {
        mUsbState.removeAll(flags);
        System.out.println("After remove flags " + flags + ", Now state is " + this.mUsbState);
    }

    public boolean checkFlagEnabled(UsbFlags flag) {
        return mUsbState.contains(flag);
    }

    public boolean checkFlagEnabled(Set flag) {
        return mUsbState.containsAll(flag);
    }

    public void printUsbState() {
        System.out.println("Current usb state is " + mUsbState);
    }
}

测试用例以及输出

public static void main(String[] args) {
  // 博客地址:wossoneri.github.io
  UsbManager usbManager = new UsbManager();
  usbManager.printUsbState();
  // 添加一项flag
  usbManager.addFlag(UsbFlags.CONNECTED);
  // 添加一组 flag
  usbManager.addFlag(EnumSet.of(UsbFlags.ACCESSORY, UsbFlags.ADB));

  // 检查存在的一个flag
  System.out.println("mUsbState contains flag " + UsbFlags.ACCESSORY + ": " +
                     usbManager.checkFlagEnabled(UsbFlags.ACCESSORY));
  // 检查不存在的一个flag
  System.out.println("mUsbState contains flag " + UsbFlags.MTP + ": " +
                     usbManager.checkFlagEnabled(UsbFlags.MTP));
  // 检查一组存在的flag
  System.out.println("mUsbState contains flag " + EnumSet.of(UsbFlags.ACCESSORY, UsbFlags.ADB) + ": " +
                     usbManager.checkFlagEnabled(EnumSet.of(UsbFlags.ACCESSORY, UsbFlags.ADB)));
  // 检查一组包含不存在的flag
  System.out.println("mUsbState contains flag " + EnumSet.of(UsbFlags.ACCESSORY, UsbFlags.MTP) + ": " +
                     usbManager.checkFlagEnabled(EnumSet.of(UsbFlags.ACCESSORY, UsbFlags.MTP)));
  // 检查一组都不存在的flag
  System.out.println("mUsbState contains flag " + EnumSet.of(UsbFlags.NCM, UsbFlags.MTP) + ": " +
                     usbManager.checkFlagEnabled(EnumSet.of(UsbFlags.NCM, UsbFlags.MTP)));

  usbManager.printUsbState();

  // 删除一个不存在的flag
  usbManager.removeFlag(UsbFlags.MTP);
  // 删除一个存在的flag
  usbManager.removeFlag(UsbFlags.ACCESSORY);
  // 删除一组都不存在的flag
  usbManager.removeFlag(EnumSet.of(UsbFlags.NCM, UsbFlags.MTP));
  // 删除一组包含不存在的flag
  usbManager.removeFlag(EnumSet.of(UsbFlags.NCM, UsbFlags.ADB));

  usbManager.addFlag(EnumSet.of(UsbFlags.ACCESSORY, UsbFlags.ADB));
  // 删除一组存在的flag
  usbManager.removeFlag(EnumSet.of(UsbFlags.ADB, UsbFlags.ACCESSORY));
}

输出为

Current usb state is []
After add flag CONNECTED, Now state is [CONNECTED]
After add flags [ACCESSORY, ADB], Now state is [CONNECTED, ACCESSORY, ADB]
mUsbState contains flag ACCESSORY: true
mUsbState contains flag MTP: false
mUsbState contains flag [ACCESSORY, ADB]: true
mUsbState contains flag [ACCESSORY, MTP]: false
mUsbState contains flag [NCM, MTP]: false
Current usb state is [CONNECTED, ACCESSORY, ADB]
After remove flag MTP, Now state is [CONNECTED, ACCESSORY, ADB]
After remove flag ACCESSORY, Now state is [CONNECTED, ADB]
After remove flags [NCM, MTP], Now state is [CONNECTED, ADB]
After remove flags [NCM, ADB], Now state is [CONNECTED]
After add flags [ACCESSORY, ADB], Now state is [CONNECTED, ACCESSORY, ADB]
After remove flags [ACCESSORY, ADB], Now state is [CONNECTED]

综上,代码唯一要注意的是

public boolean checkFlagEnabled(Set flag)

传入参数使用了Set接口,这是考虑到可能会传入其他Set的实现类型,所以传入接口参数要好于实现类型参数。

最后,EnumSet类集成了位域自身的简洁性和性能优势,又拥有枚举的所有优点,所以使用它代替位域是非常好的选择。


文章作者: Wossoneri
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明来源 Wossoneri !
评论
  目录